using Ionic.Zip;
using Pdf2epub.ExtractStrategy;
using Pdf2epub.Helpers;
using Pdf2epub.Interfaces;
using Pdf2epub.Model;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.Serialization;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Xml;

namespace Pdf2epub.BusinessLogic
{
    public class Controler : IControler
    {
        public const string ImageOutputSubPath = "Images";
        private IExtractor m_Extractor;

        private IProjectContext m_ProjectContext;

        public Controler()
        {
            m_ProjectContext = new ProjectContext(this);
        }

        public string HtmlFolder
        {
            get
            {
                string pdfPathDir = Path.GetDirectoryName(m_ProjectContext.ProjectConfiguration.PdfPath);
                string fileName = Path.GetFileNameWithoutExtension(m_ProjectContext.ProjectConfiguration.PdfPath);

                return string.Format("{0}/html_{1}", pdfPathDir, fileName);
            }
        }

        public int NumberOfPages
        {
            get
            {
                if (m_Extractor == null)
                    return 0;
                return m_Extractor.NumberOfPages;
            }
        }

        public IProjectContext ProjectContext
        {
            get { return m_ProjectContext; }
        }

        public async void AssignFont(ContentBlock contentBlock, HtmlReplacement htmlReplacement)
        {
            m_ProjectContext.ProjectConfiguration.FontAssignmentDataCollection[contentBlock.Font].HtmlReplacementStyle = htmlReplacement.StyleName;
            await UpdateContentBlocksWithChangedFontAssignmentAsync(htmlReplacement);
        }

        public async void AssignFont(FontAssignmentData fontAssignmentData, HtmlReplacement htmlReplacement)
        {
            fontAssignmentData.HtmlReplacementStyle = htmlReplacement.StyleName;
            await UpdateContentBlocksWithChangedFontAssignmentAsync(htmlReplacement);
        }

        public void DeleteContentBlockAtIndex(int selectedIndex)
        {
            m_ProjectContext.Content.RemoveAt(selectedIndex);
        }

        public void ExtractFromSource()
        {
            m_Extractor.CreateContentInContentCollection();

            using (m_ProjectContext.BulkService.Capsule())
            {
                ChapterDetection chapterDetection = new ChapterDetection(m_ProjectContext);
                chapterDetection.SetChaptersFromDetectedHeaders();
            }
        }

        public Task ExtractFromSourceAsync()
        {
            Task task = new Task(ExtractFromSource);
            task.Start();
            return task;
        }

        public string GetHtml(ContentBlock[] contentBlocks)
        {
            HtmlRenderEngine renderEngine = new HtmlRenderEngine(m_ProjectContext);
            return renderEngine.RenderHtmlFromContentBlocks(contentBlocks).InnerXml;
        }

        public Task<string> GetHtmlAsync(ContentBlock[] contentBlocks)
        {
            Task<string> task = new Task<string>(() => GetHtml(contentBlocks));
            task.Start();
            return task;
        }

        public void InsertEmptyTableBlockAtIndex(int selectedIndex)
        {
            int insertAt = selectedIndex;
            if (insertAt < m_ProjectContext.Content.Count)
                insertAt++;

            ContentBlock existingBlock = m_ProjectContext.Content[selectedIndex];
            ContentBlock newBlock = new ContentBlock(m_ProjectContext, ContentType.Table, string.Empty)
            {
                SourcePage = existingBlock.SourcePage,
                Chapter = existingBlock.Chapter,
                FirstChunkBaseLine = existingBlock.FirstChunkBaseLine,
                LastChunkBaseLine = existingBlock.LastChunkBaseLine,
                OriginalXPosition = existingBlock.OriginalXPosition
            };
            newBlock.SubBlocks.Add(new ContentBlock(ProjectContext, ContentType.TableCaption, "<new caption>"));
            newBlock.SubBlocks.Add(CreateEmptyTableRow(2));
            newBlock.SubBlocks.Add(CreateEmptyTableRow(2));

            newBlock.SequenceNumber = existingBlock.SequenceNumber + 1;

            m_ProjectContext.Content.Insert(insertAt, newBlock);

            RenumberSequenceNumbers();
        }

        public void InsertEmptyTextBlockAtIndex(int selectedIndex)
        {
            int insertAt = selectedIndex;
            if (insertAt < m_ProjectContext.Content.Count)
                insertAt++;

            ContentBlock existingBlock = m_ProjectContext.Content[selectedIndex];
            ContentBlock newBlock = new ContentBlock(m_ProjectContext, ContentType.Text, string.Empty)
            {
                SourcePage = existingBlock.SourcePage,
                Chapter = existingBlock.Chapter,
                FirstChunkBaseLine = existingBlock.FirstChunkBaseLine,
                Font = existingBlock.Font,
                LastChunkBaseLine = existingBlock.LastChunkBaseLine,
                OriginalXPosition = existingBlock.OriginalXPosition
            };

            m_ProjectContext.Content.Insert(insertAt, newBlock);

            RenumberSequenceNumbers();
        }

        public void InsertTableColumnIntoSubBlockCollection(ContentBlock tableBlock)
        {
            if (tableBlock.ContentType != ContentType.Table)
                return;

            foreach (ContentBlock row in tableBlock.SubBlocks.Where(row => row.ContentType == ContentType.TableRow))
            {
                row.SubBlocks.Add(new ContentBlock(tableBlock.ProjectContext, ContentType.TableCell));
            }
        }

        public void InsertTableRowIntoSubBlockCollection(ContentBlock tableBlock)
        {
            if (tableBlock.ContentType != ContentType.Table)
                return;

            int columnCount = TableHelper.GetColumnCount(tableBlock);
            ContentBlock newRow = new ContentBlock(tableBlock.ProjectContext, ContentType.TableRow);
            for (int i = 0; i < columnCount; i++)
            {
                newRow.SubBlocks.Add(new ContentBlock(tableBlock.ProjectContext, ContentType.TableCell));
            }

            tableBlock.SubBlocks.Add(newRow);
        }

        public void JoinBlocks(ContentBlock contentBlock)
        {
            contentBlock.JoinWithPreviousBlock = true;
        }

        public void JoinMarkedBlocks()
        {
            while (true)
            {
                ContentBlock block = m_ProjectContext.Content.LastOrDefault(b => b.JoinWithPreviousBlock);
                if (block == null)
                    break;

                int idx = m_ProjectContext.Content.IndexOf(block);
                if (idx < 1)
                    break;

                ContentBlock targetBlock = m_ProjectContext.Content[idx - 1];
                targetBlock.Content = targetBlock.Content + block.Content;

                m_ProjectContext.Content.Remove(block);
            }
        }

        public void OpenPdf(string path)
        {
            m_ProjectContext = new ProjectContext(this, LoadSavedConfig(path));

            using (m_ProjectContext.BulkService.Capsule())
            {
                m_ProjectContext.ProjectConfiguration.PdfPath = path;

                m_Extractor = ExtractorFactory.GetExtractor(m_ProjectContext, path, MakeTargetPath(path));
                if (m_Extractor == null)
                {
                    throw new InvalidFileFormatException(string.Format(
                        "No matching extraction method found for file '{0}'", path));
                }

                m_Extractor.ContentBlocks = m_ProjectContext.Content;

                m_ProjectContext.ProjectConfiguration.NumberOfPages = NumberOfPages;

                LoadCachedContent();

                m_ProjectContext.ResetSavedState();
            }
        }

        public Task OpenPdfAsync(string path)
        {
            Task openTask = new Task(() => OpenPdf(path));
            openTask.Start();
            return openTask;
        }

        public void OverrideAssignment(ContentBlock contentBlock, HtmlReplacement htmlReplacement)
        {
            contentBlock.StyleName = htmlReplacement.StyleName;
        }

        public void OverrideHtmlTag(ContentBlock contentBlock, HtmlReplacementTag tag)
        {
            contentBlock.HtmlTag = tag;
        }

        public void RemoveDuplicateWhitespaceFromParagraphBlocks()
        {
            ContentBlock[] allNonFormattedTextBlocks = m_ProjectContext.Content.Where(block => ((block.ContentType == ContentType.Text) && (!GetStyle(block.StyleName).RunRulePreformat))).ToArray();
            foreach (ContentBlock block in allNonFormattedTextBlocks)
            {
                block.Content = block.Content.Replace('\t', ' ');
                while (block.Content.Contains("  "))
                {
                    block.Content = block.Content.Replace("  ", " ");
                }
            }
        }

        public void RemoveEmptyContentBlocks()
        {
            ContentBlock[] emptyBlocks = m_ProjectContext.Content.Where(block => ((block.ContentType == ContentType.Text) && (block.Content.Trim().Length == 0))).ToArray();
            foreach (ContentBlock emptyBlock in emptyBlocks)
            {
                m_ProjectContext.Content.Remove(emptyBlock);
            }
        }

        public void RemoveLastColumnFromSubBlockCollection(ContentBlock tableBlock)
        {
            if (tableBlock.ContentType != ContentType.Table)
                return;

            foreach (ContentBlock row in tableBlock.SubBlocks.Where(row => row.ContentType == ContentType.TableRow))
            {
                ContentBlock lastCell = row.SubBlocks.LastOrDefault(block => block.ContentType == ContentType.TableCell);
                if (lastCell != null)
                {
                    row.SubBlocks.Remove(lastCell);
                }
            }
        }

        public void RemoveLastRowFromSubBlockCollection(ContentBlock tableBlock)
        {
            if (tableBlock.ContentType != ContentType.Table)
                return;

            ContentBlock lastRow = tableBlock.SubBlocks.LastOrDefault(row => row.ContentType == ContentType.TableRow);
            if (lastRow != null)
            {
                tableBlock.SubBlocks.Remove(lastRow);
            }
        }

        public void RemoveMarkedBlocks()
        {
            ContentBlock[] removeList = m_ProjectContext.Content.Where(block => block.HtmlTag == HtmlReplacementTag.Remove).ToArray();
            foreach (ContentBlock block in removeList)
            {
                m_ProjectContext.Content.Remove(block);
            }
        }

        public Task RunFontAnalysisAsync()
        {
            Task task = new Task(RunFontAnalysis);
            task.Start();
            return task;
        }

        public void Save()
        {
            SaveConfigForCurrentPdf();
            SaveCachedContent();
        }

        public void TransformHtmlToEpub()
        {
            string pdfDirectory = Path.GetDirectoryName(m_ProjectContext.ProjectConfiguration.PdfPath);
            string htmlFolder = HtmlFolder;
            string epubOutputPath = Path.Combine(pdfDirectory, Path.Combine(string.Format("{0}.epub", Path.GetFileNameWithoutExtension(m_ProjectContext.ProjectConfiguration.PdfPath))));

            EpubExporter exporter = new EpubExporter(m_ProjectContext, htmlFolder) { CoverHref = m_ProjectContext.ProjectConfiguration.CoverImagePath };
            exporter.Export(epubOutputPath);
        }

        public Task TransformHtmlToEpubAsync()
        {
            Task task = new Task(TransformHtmlToEpub);
            task.Start();
            return task;
        }

        public void TransformToHtml(string targetFolder)
        {
            HtmlRenderEngine renderEngine = new HtmlRenderEngine(m_ProjectContext)
            {
                ExportMode = HtmlRenderEngine.FileExportMode.SplitAtCapters
            };
            renderEngine.TransformToHtml(targetFolder);
        }

        public Task TransformToHtmlAsync(string targetFolder)
        {
            Task task = new Task(() => TransformToHtml(targetFolder));
            task.Start();
            return task;
        }

        public void UnJoinBlocks(ContentBlock contentBlock)
        {
            contentBlock.JoinWithPreviousBlock = false;
        }

        private static DataContractSerializer GetContentSerializer()
        {
            DataContractSerializer serializer = new DataContractSerializer(typeof(ContentBlockCollection),
                                                                           null,
                                                                           int.MaxValue /*maxItemsInObjectGraph*/,
                                                                           false /*ignoreExtensionDataObject*/,
                                                                           true
                /*preserveObjectReferences : this is where the magic happens */,
                                                                           null /*dataContractSurrogate*/);
            return serializer;
        }

        private static string MakeTargetPath(string path)
        {
            string targetDirectory = Path.GetDirectoryName(path);
            string sourceFileName = Path.GetFileNameWithoutExtension(path);
            return Path.Combine(targetDirectory, string.Format("html_{0}", sourceFileName));
        }

        private static void SaveArchiveFileChunk(string contentFileName, ContentBlockCollection tempSaveCollection)
        {
            using (XmlWriter xmlWriter = new XmlTextWriter(contentFileName, Encoding.UTF8))
            {
                DataContractSerializer serializer = GetContentSerializer();
                serializer.WriteObject(xmlWriter, tempSaveCollection);
            }
        }

        private void AppendContentFromArchivedFileChunk(string extractedFile)
        {
            using (XmlReader xmlReader = XmlReader.Create(extractedFile))
            {
                DataContractSerializer serializer = GetContentSerializer();
                ContentBlockCollection loadedContent =
                    serializer.ReadObject(xmlReader) as ContentBlockCollection;

                SetContextToContentBlock(loadedContent);

                m_ProjectContext.Content.Add(loadedContent);
            }
        }

        private ContentBlock CreateEmptyTableRow(int columnCount)
        {
            ContentBlock row = new ContentBlock(ProjectContext, ContentType.TableRow);
            for (int i = 0; i < columnCount; i++)
            {
                row.SubBlocks.Add(new ContentBlock(ProjectContext, ContentType.TableCell, "<cell>"));
            }
            return row;
        }

        private string GetCacheFile(string pdfPath)
        {
            string baseDir = Path.GetDirectoryName(pdfPath);
            string filename = Path.GetFileNameWithoutExtension(pdfPath);
            return string.Format("{0}/{1}.content", baseDir, filename);
        }

        private string GetConfigPath(string sourcePath)
        {
            if (string.IsNullOrEmpty(sourcePath))
                return string.Empty;

            string configPath = string.Format("{0}\\{1}.pdf2epub",
                Path.GetDirectoryName(sourcePath),
                Path.GetFileNameWithoutExtension(sourcePath));
            return configPath;
        }

        private string GetFileChunkName(int blockFile)
        {
            string baseDir = Path.GetDirectoryName(m_ProjectContext.ProjectConfiguration.PdfPath);
            string filename = Path.GetFileNameWithoutExtension(m_ProjectContext.ProjectConfiguration.PdfPath);
            string contentFileName = string.Format("{0}/{1}.{2}.content", baseDir, filename, blockFile);
            return contentFileName;
        }

        private HtmlReplacement GetStyle(string styleName)
        {
            if (ProjectContext.ProjectConfiguration.HtmlReplacements.Contains(styleName))
                return ProjectContext.ProjectConfiguration.HtmlReplacements[styleName];
            return ProjectContext.ProjectConfiguration.HtmlReplacements[".none"];
        }

        private void InsertFontIntoReplacementConfig(FontAssignmentData fontAssignment)
        {
            if (!m_ProjectContext.ProjectConfiguration.FontAssignmentDataCollection.Contains(fontAssignment.FontUsage))
            {
                Application.Current.Dispatcher.Invoke(() => m_ProjectContext.ProjectConfiguration.FontAssignmentDataCollection.Add(fontAssignment));
            }
            else
            {
                Application.Current.Dispatcher.Invoke(() => m_ProjectContext.ProjectConfiguration.FontAssignmentDataCollection[fontAssignment].Occurance = fontAssignment.Occurance);
            }
        }

        private void LoadCachedContent()
        {
            string filename = GetCacheFile(m_ProjectContext.ProjectConfiguration.PdfPath);
            if (File.Exists(filename))
            {
                try
                {
                    m_ProjectContext.Content.Clear();
                    string tempPath = Path.GetTempPath();
                    using (ZipFile archive = new ZipFile(filename))
                    {
                        foreach (var entry in archive.Entries)
                        {
                            string extractedFile = Path.Combine(tempPath, entry.FileName);
                            if (File.Exists(extractedFile))
                                File.Delete(extractedFile);

                            entry.Extract(tempPath);

                            if (File.Exists(extractedFile))
                            {
                                AppendContentFromArchivedFileChunk(extractedFile);

                                File.Delete(extractedFile);
                            }
                        }
                    }
                }
                catch { }
            }
        }

        private ProjectConfiguration LoadSavedConfig(string path)
        {
            string configFile = GetConfigPath(path);
            if (!File.Exists(configFile))
                return new ProjectConfiguration();

            return ProjectConfiguration.LoadFromFile(configFile);
        }

        private void RenumberSequenceNumbers()
        {
            for (int i = 0; i < m_ProjectContext.Content.Count; i++)
            {
                m_ProjectContext.Content[i].SequenceNumber = i;
            }
        }

        private void RunFontAnalysis()
        {
            m_ProjectContext.FeedbackService.SetBusyText("Running font analysis...");

            IFontAnalyzer analyzer = FontAnalyzerFactory.GetAnalyzer(m_ProjectContext);
            FontAnalysisResult result = analyzer.Analzye();

            m_ProjectContext.FeedbackService.SetDetailText("Rendering results...");

            foreach (FontUsage fontUsage in result.FontUsages)
            {
                FontAssignmentData fontAssignment = new FontAssignmentData
                {
                    FontUsage = fontUsage,
                    HtmlReplacementStyle = ".none"
                };

                InsertFontIntoReplacementConfig(fontAssignment);
            }

            m_ProjectContext.ProjectConfiguration.AverageLineDistance = result.AverageLineDistance;
            m_ProjectContext.ProjectConfiguration.MinimumLineDistance = result.MinimumLineDistance;
            m_ProjectContext.ProjectConfiguration.MaximumLineDistance = result.MaximumLineDistance;
            m_ProjectContext.ProjectConfiguration.MostlyUsedLineDistance = result.MostlyUsedLineDistance;
        }

        private void SaveCachedContent()
        {
            if (m_ProjectContext.Content.Count == 0)
                return;

            int numberOfBlocks = m_ProjectContext.Content.Count;
            int currentStartBlock = 0;
            int blocksToStore = 1000;
            int blockFile = 0;

            string archFile = GetCacheFile(m_ProjectContext.ProjectConfiguration.PdfPath);
            if (File.Exists(archFile))
                File.Delete(archFile);

            using (ZipFile archive = new ZipFile(archFile))
            {
                while (currentStartBlock < numberOfBlocks)
                {
                    blockFile++;

                    var tempSaveCollection = new ContentBlockCollection
                    {
                        m_ProjectContext.Content.Skip(currentStartBlock).Take(blocksToStore)
                    };

                    string contentFileName = GetFileChunkName(blockFile);

                    SaveArchiveFileChunk(contentFileName, tempSaveCollection);

                    archive.AddFile(contentFileName, string.Empty);
                    archive.Save();

                    File.Delete(contentFileName);

                    currentStartBlock = currentStartBlock + blocksToStore;
                }
            }
        }

        private void SaveConfigForCurrentPdf()
        {
            if (string.IsNullOrEmpty(m_ProjectContext.ProjectConfiguration.PdfPath))
                return;

            m_ProjectContext.ProjectConfiguration.WriteToFile(GetConfigPath(m_ProjectContext.ProjectConfiguration.PdfPath));
        }

        private void SetContextToContentBlock(IEnumerable<ContentBlock> contentBlocks)
        {
            foreach (var block in contentBlocks)
            {
                block.SetProjectContext(m_ProjectContext);
                SetContextToContentBlock(block.SubBlocks);
            }
        }

        private void UpdateContentBlocksWithChangedFontAssignment(HtmlReplacement htmlReplacement)
        {
            using (m_ProjectContext.BulkService.Capsule())
            {
                IEnumerable<ContentBlock> blocksWithReplacement = m_ProjectContext.Content.Where(block => Equals(block.DefaultReplacement, htmlReplacement));
                foreach (var block in blocksWithReplacement)
                {
                    block.TriggerRefresh("Font");
                }
            }
        }

        private Task UpdateContentBlocksWithChangedFontAssignmentAsync(HtmlReplacement htmlReplacement)
        {
            Task task = new Task(() => UpdateContentBlocksWithChangedFontAssignment(htmlReplacement));
            task.Start();
            return task;
        }
    }

    public class InvalidFileFormatException : Exception
    {
        public InvalidFileFormatException(string message)
            : base(message)
        {
        }
    }
}